Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(orchestration): return unwrapped vows #9454

Merged
merged 12 commits into from
Jun 19, 2024
Merged

Conversation

0xpatrickdev
Copy link
Member

@0xpatrickdev 0xpatrickdev commented Jun 5, 2024

refs: #9449

Description

  • convert icqConnectionKit to return vows
  • convert Orchestration service (service.js) to return vows
  • convert chainAccountKit to return vows

Security Considerations

Scaling Considerations

These changes are necessary towards supporting using orchestration in asyncFlow.

Documentation Considerations

Testing Considerations

Upgrade Considerations

Copy link

cloudflare-workers-and-pages bot commented Jun 5, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: c0fd411
Status: ✅  Deploy successful!
Preview URL: https://95a4f77f.agoric-sdk.pages.dev
Branch Preview URL: https://pc-9449-icq-connection-vows.agoric-sdk.pages.dev

View logs

),
provideICQConnection: M.callWhen(M.string()).returns(
M.remotable('Connection'),
makeAccount: M.call(M.string(), M.string()).returns(M.promise()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why lose the validation of the return type?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makeAccount is no longer an async function, so I didn't think .callWhen() was appropriate. Reverted in 1471410

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that .callWhen is not simple validation and indeed make the function become async. @erights for confirmation, but we may need to manually validate this, or find a way to make this pattern check compatible with upgradable methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the problem whether an await happens in the method? If the JS code that this is guarding doesn't have the async keyword then it can't await.

How could the guard change that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is doing any promise "then" if the promise may not be resolved until a future crank. Whether that's done by an await, by a when or any other mechanism that under the hood wraps promise.then(). callWhen is such a mechanism as, from what I understand, it internally when the arguments and delays the invocation of the user function until after the arguments have settled.

),
});

const requestICAConnectionWatcherI = M.interface(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since these aren't exported, I think they would read better inlined..

if you don't then please capitalize the way other interface guards are, StudlyCaps

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await E(port).connect(
remoteConnAddr,
chainAccountKit.connectionHandler,
const portAllocator = getPower(this.state.powers, 'portAllocator');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of scope, but what motivates this getPower abstraction? it makes it harder to review.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a comment here:

* PowerStore is used so additional powers can be added on upgrade. See
* [#7337](https://github.com/Agoric/agoric-sdk/issues/7337) for tracking on Exo
* state migrations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, that makes sense. I was looking on getPower.

So that explains why a store instead of a record, and I take it the getter is because maps assume homogeneity of value types. That could be worthwhile to include as a comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, going forward s/7337/7407 #7337 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we talked about getPower and how to get rid of it, right, @0xpatrickdev ? (IOU link)
we don't need to complicate access to known state fields to provide for expansion.
we just need one reserved-for-future-use property

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@0xpatrickdev 0xpatrickdev Jun 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dckc . I'll tackle this in a follow-up PR

Comment on lines +207 to +204
return when(
watch(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're always going to have to when(watch( is there a worthwhile convenience function to make? It's fine to chain them but it ends up increasing the indentation.

maybe a helper facet could manage it? perhaps validated automatically against the watcher facet

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we ultimately only want to return watch. when is a stop gap during the refactor so we still return a promise.

import { makeDurableZone } from '@agoric/zone/durable.js';
import { prepareOrchestrationTools } from './service.js';

/** @import {OrchestrationPowers} from './service.js' */

export const buildRootObject = (_vatPowers, _args, baggage) => {
const zone = makeDurableZone(baggage);
const vowTools = prepareVowTools(zone.subZone('VowTools'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the subzone necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. I was following the example here:

## Durability
The `@agoric/vow/vat.js` module allows vows to integrate Agoric's vat upgrade
mechanism. To create vow tools that deal with durable objects:
```js
// NOTE: Cannot use `V` as it has non-durable internal state when unwrapping
// vows. Instead, use the default vow-exposing `E` with the `watch`
// operator.
import { E } from '@endo/far';
import { prepareVowTools } from '@agoric/vow/vat.js';
import { makeDurableZone } from '@agoric/zone';
// Only do the following once at the start of a new vat incarnation:
const zone = makeDurableZone(baggage);
const vowZone = zone.subZone('VowTools');
const { watch, makeVowKit } = prepareVowTools(vowZone);
// Now the functions have been bound to the durable baggage.
// Vows and resolvers you create can be saved in durable stores.
```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for the network and ibc vats, we've followed the pattern of creating a subzone specifically for all vow-related data

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? What happens if you don't make a subzone?

@0xpatrickdev 0xpatrickdev marked this pull request as ready for review June 5, 2024 01:25
@0xpatrickdev 0xpatrickdev requested a review from mhofman June 5, 2024 01:26
Copy link
Member

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this caused me to look a little deeper into the vow implementation, which raised some questions for @michaelfig , but I took those offline.

My main concern is with how synchronous errors now end up in throws instead of vow rejections.

async close() {
/// XXX what should the behavior be here? and `onClose`?
close() {
/// TODO #9192 what should the behavior be here? and `onClose`?
// - retrieve assets?
// - revoke the port?
const { connection } = this.state;
if (!connection) throw Fail`connection not available`;
Copy link
Member

@mhofman mhofman Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok this is interesting. A good API design is to not throw synchronously if you return a promise. Previously because the function was async, the call would return a rejected promise with this error. The equivalent would be to return a rejected vow, but in general, this is very cumbersome to get right all the time. I am wondering if we don't need to revive something like the promise constructor pattern and wrap all synchronous prelude into a helper.

Something along the lines of

/**
 * @template {any} T
 * @param {(...args: any[]) => Vow<Awaited<T>> | Awaited<T>} fn
 * @returns {Vow<Awaited<T>>}
 */
const asVow = fn => {
  const kit = makeVowKit();
  try {
    kit.resolver.resolve(fn());
  } catch(e) {
    kit.resolver.reject(e);
  }
  return kit.vow;
};

and then

        close() {
          return when(asVow(() => {
            /// TODO #9192 what should the behavior be here? and `onClose`?
            // - retrieve assets?
            // - revoke the port?
            const { connection } = this.state;
            if (!connection) throw Fail`connection not available`;
            return watch(E(connection).close());
          }));
        },

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for highlighting this. I’ve added comments in areas where we can fail synchronously referencing this comment and the parent issue in 40290c1.

I suppose the same would also apply for something like provideICQConnection, where we can return synchronously early?

 /**
   * @param {IBCConnectionID} controllerConnectionId
   * @returns {ICQConnection | Promise<ICQConnection>}
   */
  provideICQConnection(controllerConnectionId) {
    if (this.state.icqConnections.has(controllerConnectionId)) {
      return this.state.icqConnections.get(controllerConnectionId)
        .connection;
    }
    const remoteConnAddr = makeICQChannelAddress(controllerConnectionId);
    const portAllocator = getPower(this.state.powers, 'portAllocator');
    return when(
      watch(
        // allocate a new Port for every Connection
        // TODO #9317 optimize ICQ port allocation
        E(portAllocator).allocateICQControllerPort(),
        this.facets.requestICQChannelWatcher,
        {
          remoteConnAddr,
          controllerConnectionId,
        },
      ),
    );
  },

Comment on lines 87 to 95
handleExecuteEncodedTxWatcher: {
/** @param {string} ack */
onFulfilled(ack) {
return parseTxPacket(ack);
},
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like it needs to be part of the kit. Why not have ../utils/packet.js provide a singleton parseTxPacketWatchHandler that can be used by all calls? But I suppose that'd still require a prepare call accepting a zone.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, taking note of this pattern for the future. For now I've decided to keep the handler as a facet on the chainAccountKit - it doesn't seem like we'll need parseTxPacket elsewhere.

I adopted your naming suggestions in 0fc3ca1 and 438446a - but am just calling it a Watcher and not a WatcherHandler for brevity.

Copy link
Contributor

@iomekam iomekam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vow implementation looking good!

Comment on lines 218 to 237
return when(
watch(
// allocate a new Port for every Connection
// TODO #9317 optimize ICQ port allocation
E(portAllocator).allocateICQControllerPort(),
this.facets.requestICQConnectionWatcher,
{
remoteConnAddr,
controllerConnectionId,
},
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of nesting the call of E(port.connect) within requestICQConnectionWatcher, you could instead take an approach like this:

const icqControllerPortWatcher = watch(E(portAllocator).allocateICQControllerPort(), this.facets.requestICQConnectionWatcher)
return when(watch(icqControllerPortWatcher, this.facets.portConnectWater, context))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/icqControllerPortWatcher/icqControllerPortVow/

@0xpatrickdev 0xpatrickdev force-pushed the pc/9449-icq-connection-vows branch 2 times, most recently from 843897c to 99c4cae Compare June 18, 2024 19:19
@0xpatrickdev 0xpatrickdev requested a review from turadg June 19, 2024 17:42
@turadg turadg force-pushed the pc/9449-icq-connection-vows branch 2 times, most recently from ed28c2c to 5fccb85 Compare June 19, 2024 19:39
@turadg turadg force-pushed the pc/9449-icq-connection-vows branch from 5fccb85 to c0fd411 Compare June 19, 2024 19:40
Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rebased this on master and fixed issue I ran into. One was a race condition with VBANK fake bridge. I put an eventLoop await in the failing tests. Longer term we'll have the getBalances() checkpoint to prevent withdrawing below zero.

I also made all the changes I would have requested. So @0xpatrickdev PTAL at whether it LGTY before you merge.

);
trace('undelegate response', response);
const { completionTime } = response;
completionTime || Fail`No completion time result ${result}`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
completionTime || Fail`No completion time result ${result}`;
completionTime || Fail`No completion time in result: ${result}`;

@0xpatrickdev
Copy link
Member Author

So @0xpatrickdev PTAL at whether it LGTY before you merge

LGTM :shipit:

@turadg turadg added the automerge:rebase Automatically rebase updates, then merge label Jun 19, 2024
@mergify mergify bot merged commit f3f87b1 into master Jun 19, 2024
87 checks passed
@mergify mergify bot deleted the pc/9449-icq-connection-vows branch June 19, 2024 20:33
@0xpatrickdev 0xpatrickdev added the asyncFlow related to membrane-based replay and upgrade of async functions label Jun 20, 2024
0xpatrickdev added a commit that referenced this pull request Jun 24, 2024
- adds asVow() helper function that coerces the result of a function to a Vow
- see comment from @mhofman for inspiration: #9454 (comment)
0xpatrickdev added a commit that referenced this pull request Jun 25, 2024
- adds asVow() helper function that coerces the result of a function to a Vow
- see comment from @mhofman for inspiration: #9454 (comment)
0xpatrickdev added a commit that referenced this pull request Jun 25, 2024
- adds asVow() helper function that coerces the result of a function to a Vow
- see comment from @mhofman for inspiration: #9454 (comment)
gibson042 pushed a commit that referenced this pull request Jul 2, 2024
- adds asVow() helper function that coerces the result of a function to a Vow
- see comment from @mhofman for inspiration: #9454 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
asyncFlow related to membrane-based replay and upgrade of async functions automerge:rebase Automatically rebase updates, then merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants